Components: themed primitives

These are Nuxt UI components with the Ledger config applied. Every specimen reads the real --ui-* variables, so the theme switch on the right changes the whole page. The product components are on a second page.

Button

There are five roles. A screen gets one primary button. Danger is used only for irreversible actions, and always inside a confirmation step.

New articleprimary
Cancelsecondary
Previewghost
Delete articledanger
icon-only
Savesm 28
Savemd 32
Savelg 40
Savexl 44, touch
Publishdefault
Publishhover
Publishfocus-visible
Publishactive
Publishdisabled
Publishing…loading
Delete articledanger hover
Cancelsecondary hover
Previewghost hover
Nuxt UIUButton. Primary color="primary" variant="solid" · secondary color="neutral" variant="outline" · ghost color="neutral" variant="ghost" · danger color="error" variant="solid" · icon-only square icon="i-lucide-ellipsis" :aria-label wrapped in UTooltip. Loading:loading swaps the leading icon for i-lucide-loader-circle and disables the button. The label changes to the -ing form ("Publishing…") and the button width stays fixed, so the layout doesn't jump. loading-auto is used on async @click handlers. Keyboard, ARIANative <button>, activated with Enter or Space. Icon-only buttons need an aria-label. Loading sets aria-busy="true". Disabled buttons stay focusable only when a tooltip explains why they're disabled. Responsivemd on desktop, xl (44 px) below md. Primary page actions go full width in sticky footers on 390 px screens. BendDisabled opacity is 40% instead of Nuxt UI's 75%, because a 75% black button still looks clickable. Hover darkens the button one step instead of using Nuxt UI's /75 alpha, so the label keeps its contrast.

Input, password input, textarea

One outline style for every field. Focus adds a 2 px ink ring inside the edge. Errors turn the edge brick red and always come with a message, never color alone.

martadefault, placeholder
martahover
marfocus
maerror
owner@renew.exampledisabled / read-only
plasterleading icon, clear
••••••••••password, hidden
limewash2026password, shown
A short first paragraph. You can keep writing in the editor.textarea, autoresize 3–8 rows
Nuxt UIUInput variant="outline" color="primary", :highlight bound to the field error so the edge turns red. The search field is icon="i-lucide-search" with a #trailing clear UButton. UTextarea autoresize :rows="3" :maxrows="8". PasswordUInput :type="shown ? 'text' : 'password'" with a #trailing ghost UButton. aria-label reads "Show password" or "Hide password", with aria-pressed and aria-controls. autocomplete is current-password on log in and new-password on register and reset. Showing the password never clears or moves focus. StatesDefault, hover (edge goes to muted), focus (2 px ink), filled, error, disabled, read-only (same look as disabled but selectable and focusable, used for the email on the reset screen). Fields are 16 px text below md. PlaceholderPlaceholders use --ui-text-muted (5.7:1) and only show an example. The label always carries the meaning.

Form field

A label on top, then the control, then the help text or the error in the same spot. The right-hand hint slot holds the character counter. Every field in the product is required, so none are marked.

Username5 / 20
marta3 to 20 characters. You'll use it to log in.
Username2 / 20
maUse at least 3 characters.
Emailmarta@renewEnter a full email address, like name@example.com.
Nuxt UIUForm :schema :state @submit + UFormField name label help, with the counter in the #hint slot. Validation runs on blur and on submit, then on every input once a field has an error, so the error clears as soon as it's fixed. The schema mirrors the API rules exactly, so a validator 400 shouldn't reach the UI. ARIANuxt UI links the label and aria-describedby (help or error) and sets aria-invalid. On a failed submit, focus moves to the first invalid field. Error text sits in the help slot, so there is no layout shift.

Checkbox, switch, select

All three are square. The Preview toggle in the editor is the only switch. The select sets sort order in the mobile article list. No screen needs a checkbox yet, but it's themed so it matches if one gets added.

Off On Focus Disabled
Preview off Preview on Focus Disabled
Newest updated first
Newest updated first
Oldest updated first
Nuxt UIUCheckbox, USwitch (slots base and thumb set to rounded-none), USelect :items value-key="value", whose content uses --shadow-popover. KeyboardSpace toggles a checkbox or switch. The select opens with Enter, Space or ↓, moves with the arrow keys and type-ahead, and closes with Esc, returning focus to the trigger. The switch uses role="switch" and aria-checked, and its visible label is its name.

Badge

Badges show status or a count. They never replace a button. Solid ink is for the one state that matters most in a list. Soft semantic badges are for system messages.

Online Draft 12 success info warning error GET Online · md
Nuxt UIUBadge. Solid ink color="neutral" variant="solid" · outline color="neutral" variant="outline" · count color="neutral" variant="soft" size="sm" · semantic variant="soft". Sizes are sm (22 px, in tables) and md (26 px, in the editor header). ARIABadges are text, not live regions. When a status changes, the announcement comes from the toast.

Alert

Alerts are for messages that belong to a page and stay until the situation changes. Anything that confirms an action someone just took is a toast instead.

Your session expiredLog in again to pick up where you left off.
Password changedLog in with your new password.
Almost at the length limit9 240 of 10 000 characters used. Images and formatting count too.
Couldn't load your articlesCheck your connection and try again.
Try again
Nuxt UIUAlert variant="soft" :color :icon :title :description :actions. close only on alerts that are safe to dismiss (password changed, session expired). The error alert has no close button because its retry is the way out. ARIAInfo, success and warning use role="status" and error uses role="alert". Alerts that are already there when the page loads aren't announced again. The description text uses --ui-text-toned rather than the semantic color, so long text stays readable.

Toast

Toasts confirm that an action finished or failed. Each one has a short title and, at most, one action.

Article published"Winter checklist for timber windows" is now online.
That article no longer existsIt was deleted somewhere else, so we removed it from the list.
Couldn't unpublishThe server didn't respond.
Retry
PlacementBottom right on desktop, 24 px from the edges. Bottom center at full width minus 16 px on mobile, above the sticky action bar. DurationSuccess and info close after 5 s. Errors with retry stay until dismissed or retried. The timer pauses on hover and on focus. StackAt most 3 visible, newest at the bottom. A fourth pushes out the oldest non-error toast. Repeating the same action replaces its toast instead of adding a new one. Nuxt UIUApp :toaster="{ position: 'bottom-right', duration: 5000, max: 3 }". Toasts are raised with useToast().add({ id, title, description, color, icon, actions, duration }), where id is the action plus the article id so a repeat replaces the old toast. Errors use duration: 0. The progress bar is off. ARIAThe Reka toast region announces politely, and errors assertively. F8 jumps to the region. Esc closes the focused toast.

Modal and slideover

Modals ask a question or hold a short form. Slideovers carry navigation and side panels on small screens. Both sit over a 40% ink overlay.

Leave without saving?You have changes to "How we price a bathroom renovation" that haven't been saved.
Discard changesKeep editing
Details
Created12 Mar 2026, 09:14UpdatedToday, 11:02ID12Images3
Nuxt UIUModal :title :description with the #footer slot for actions, width sm:max-w-md. Opened programmatically with useOverlay().create(ConfirmDialog). USlideover side="right" for details, side="left" for mobile navigation (built into UDashboardSidebar). Button orderThe safe action is primary and sits on the right, where the eye lands. In destructive confirmations, the danger button is on the right and Cancel on the left (see ConfirmDialog). On mobile the buttons stack full width, with the primary on top. Keyboard, ARIAFocus is trapped and starts on the least destructive button. Esc and the overlay close it unless it's :dismissible="false", which is used while an action is running. Focus returns to the trigger. The title is the aria-labelledby and the description is the aria-describedby. ResponsiveBelow sm, modals sit 16 px from the screen edges and slideovers take the full width. Motion follows the foundations: 240 ms, and instant with reduced motion.

Dropdown menu and tooltip

The row-action menu is the main dropdown. Delete always comes last, below a separator, in brick red. Tooltips name icon-only buttons and show their keyboard shortcut.

Edit
Unpublish
Copy API URL
Delete…
BoldCtrlB
Nuxt UIUDropdownMenu :items :content="{ align: 'end' }", with items grouped in nested arrays so separators appear automatically. The Delete item is color: 'error', and its label ends in "…" because it opens a confirmation. UTooltip :text :kbds="['ctrl', 'b']" :delay-duration="400". KeyboardThe menu opens with Enter, Space or ↓. Arrow keys and type-ahead move through it, Home and End jump to the ends, and Esc closes it and returns focus. Tooltips appear on focus as well as hover and never hold information that isn't also in the aria-label. TouchTooltips don't show on touch, so every icon-only control on mobile also appears in a labeled menu or has a visible label.

Tabs

Tabs filter the article list. Each count comes from the full list, so it doesn't change while you search.

All6 Online4 Drafts2 Focused
From left: selected, default, hover, focus-visible.
Nuxt UIUTabs variant="link" color="neutral" :content="false" :items v-model. The count goes in the #trailing slot as the mono sm count badge. The value syncs to ?status=online|draft so the filtered view can be linked. KeyboardRoving tabindex: Tab enters the tab list on the selected tab, and ← and → move and activate. Each tab's accessible name includes its count, for example "Drafts, 2 articles". ResponsiveTabs stay in one row at 360 px, which is why the labels are short. Each tab is 44 px tall on touch.

Table and pagination

Rows are 44 px with hairline separators, and only the Updated column sorts. Pagination appears only when a filter shows more than 25 articles.

TitleStatusUpdated
How we price a bathroom renovationdefault2 hours ago
Choosing lime plaster for old stone wallshoverYesterday
Before and after: the Rue Verte kitchenfocus-visible3 days ago
Winter checklist for timber windowsbusy5 days ago
26–50 of 142
123…6
Nuxt UIUTable :data :columns v-model:sorting v-model:global-filter v-model:pagination :loading with TanStack getPaginationRowModel, and @select for row click. UPagination :total :items-per-page="25" :sibling-count="1" show-edges, active item color="primary" variant="solid". StatesDefault, hover (bg-elevated), focus-visible (2 px inset ring), busy (a row action is running: 50% opacity, a spinner in the status cell, aria-busy, actions disabled), loading (skeleton rows), empty (EmptyState in the #empty slot). Keyboard, ARIAEach row's title is a real link to the editor, so Tab reaches it and Enter opens it. The rest of the row is a click target for the mouse only. The sort header is a button with aria-sort. Pagination is a nav labeled "Pages", and the current page has aria-current.

Skeleton, avatar, kbd, separator

MAmarta
JVjan_vries
BObob
Ctrl S Save
SkeletonUSkeleton blocks shaped like the real layout: the same row heights, a badge-sized block and a date-sized block. The pulse runs at 1.6 s and stops with reduced motion. The container is aria-busy="true" and a visually hidden live region says "Loading articles". Skeletons appear only after 150 ms, so fast responses never flash. AvatarUAvatar :alt="username" size="xs|sm". The initials come from the username: split on anything that isn't a letter or digit, take the first letter of the first two parts, or the first two letters if there is only one part, then uppercase. Sand #d4c5b0 with ink text (11.2:1) in both themes. It's decorative (aria-hidden) because the username is always shown next to it. KbdUKbd variant="outline", only inside tooltips and the editor's shortcut hints. The editor shortcuts are Ctrl/⌘ S to save, plus TipTap's defaults: B, I, U, Z and Shift Z. SeparatorUSeparator uses --ui-border by default and --ui-border-accented between groups in dense toolbars. It's decorative, with role="none", except between menu groups.

Color mode

Three choices: System (the default), Light and Dark. The toggle at the top of this page is the component itself, so you can try it.

Nuxt UIIn the user menu: a UDropdownMenu group of three type: 'checkbox' items bound to useColorMode().preference. On auth screens: UColorModeSelect in the footer. Nuxt UI's UColorModeButton isn't used because it only toggles between light and dark and hides the system option. colorMode: true in the plugin handles storage and applying the .dark class before first paint. ARIAMenu items use menuitemcheckbox, with aria-checked on the current choice. The segmented variant is a radiogroup named "Theme".